﻿/**
 * Copyright (C) 2008, Microsoft Corp.  All Rights Reserved
 */

using System;
using System.Collections.Generic;
using System.Data.Entity.Design;
using System.Data.Mapping;
using System.Data.Metadata.Edm;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using ConceptualEdmGen;

namespace EdmGen2
{
    /// <summary>
    /// 
    /// This is a command-line program to perform some common Entity Data 
    /// Model tooling functions on EDMX files.  It is similiar in functionality 
    /// to the .net framework's EdmGen.exe, but it will operate on the ".edmx" 
    /// file format, instead of the .csdl, .ssdl & .msl file formats used by the 
    /// .net framework's EDM.
    /// 
    /// </summary>
    class EdmGen2
    {
        static string csdlNamespace = "http://schemas.microsoft.com/ado/2006/04/edm";
        static string ssdlNamespace = "http://schemas.microsoft.com/ado/2006/04/edm/ssdl";
        static string mslNamespace = "urn:schemas-microsoft-com:windows:storage:mapping:CS";

        internal enum Mode { FromEdmx, ToEdmx, ModelGen, CodeGen, ViewGen, Validate, RetrofitModel, Help }

        static void Main(string[] args)
        {
            if (args.Length < 1)
            {
                ShowUsage();
                return;
            }

            Mode mode = GetMode(args[0]);

            switch (mode)
            {
                case Mode.FromEdmx:
                    FromEdmx(args);
                    break;
                case Mode.ToEdmx:
                    ToEdmx(args);
                    break;
                case Mode.ModelGen:
                    ModelGen(args);
                    break;
                case Mode.CodeGen:
                    CodeGen(args);
                    break;
                case Mode.ViewGen:
                    ViewGen(args);
                    break;
                case Mode.Validate:
                    Validate(args);
                    break;
                case Mode.RetrofitModel:
                    RetrofitModel(args);
                    break;
                default:
                    ShowUsage();
                    return;
            }
        }

        private static void ShowUsage()
        {
            Console.WriteLine("Usage:  EdmGen2 [arguments]");
            Console.WriteLine("                 /FromEdmx <edmx file>");
            Console.WriteLine("                 /ToEdmx <csdl file> <msl file> <ssdl file>");
            Console.WriteLine("                 /ModelGen <connection string> <provider name> <model name>");
            Console.WriteLine("                 /RetrofitModel <connection string> <provider name> <model name> <percent threshold>?");
            Console.WriteLine("                 /ViewGen cs|vb <edmx file>");
            Console.WriteLine("                 /CodeGen cs|vb <edmx file>");
            Console.WriteLine("                 /Validate <edmx file>");
        }

        #region the functions that actually do the interesting things

        private static void FromEdmx(string[] args)
        {
            FileInfo edmxFile;
            if (ParseEdmxFileArguments(args[1], out edmxFile))
            {
                FromEdmx(edmxFile);
            }
        }

        private static void FromEdmx(FileInfo edmxFile)
        {
            XDocument xdoc = XDocument.Load(edmxFile.FullName);

            // select the csdl element, and write it out
            XElement csdl = GetCsdlFromEdmx(xdoc);
            string csdlFileName = GetFileNameWithNewExtension(edmxFile, ".csdl");
            File.WriteAllText(csdlFileName, csdl.ToString());

            // select the ssdl element and write it out
            XElement ssdl = GetSsdlFromEdmx(xdoc);
            string ssdlFileName = GetFileNameWithNewExtension(edmxFile, ".ssdl");
            File.WriteAllText(ssdlFileName, ssdl.ToString());

            // select the msl element and write it out
            XElement msl = GetMslFromEdmx(xdoc);
            string mslFileName = GetFileNameWithNewExtension(edmxFile, ".msl");
            File.WriteAllText(mslFileName, msl.ToString());
        }

        private static void ModelGen(string[] args)
        {
            if (args.Length != 4)
            {
                System.Console.WriteLine("Usage:  ModelGenerator <connection string> <provider name> <model name>");
                return;
            }
            string connectionString = args[1];
            string provider = args[2];
            string modelName = args[3];
            ModelGen(connectionString, provider, modelName);
        }

        private static void ModelGen(
            string connectionString, string provider, string modelName)
        {
            IList<EdmSchemaError> ssdlErrors = null;
            IList<EdmSchemaError> csdlAndMslErrors = null;

            // generate the SSDL
            string ssdlNamespace = modelName + "Model.Store";
            EntityStoreSchemaGenerator essg =
                new EntityStoreSchemaGenerator(
                    provider, connectionString, ssdlNamespace);
            ssdlErrors = essg.GenerateStoreMetadata();

            // write out errors
            if ((ssdlErrors != null && ssdlErrors.Count > 0))
            {
                System.Console.WriteLine("Errors occurred during generation:");
                WriteErrors(ssdlErrors);
                return;
            }

            // write the SSDL to a string
            StringWriter ssdl = new StringWriter();
            XmlWriter ssdlxw = XmlWriter.Create(ssdl);
            essg.WriteStoreSchema(ssdlxw);
            ssdlxw.Flush();

            // generate the CSDL
            string csdlNamespace = modelName + "Model";
            string csdlEntityContainerName = modelName + "Entities";
            EntityModelSchemaGenerator emsg =
                new EntityModelSchemaGenerator(
                    essg.EntityContainer, csdlNamespace, csdlEntityContainerName);
            csdlAndMslErrors = emsg.GenerateMetadata();

            // write out errors
            if (csdlAndMslErrors != null && csdlAndMslErrors.Count > 0)
            {
                System.Console.WriteLine("Errors occurred during generation:");
                WriteErrors(csdlAndMslErrors);
                return;
            }

            // write CSDL to a string
            StringWriter csdl = new StringWriter();
            XmlWriter csdlxw = XmlWriter.Create(csdl);
            emsg.WriteModelSchema(csdlxw);
            csdlxw.Flush();

            // write MSL to a string
            StringWriter msl = new StringWriter();
            XmlWriter mslxw = XmlWriter.Create(msl);
            emsg.WriteStorageMapping(mslxw);
            mslxw.Flush();

            // write csdl, ssdl & msl to the EDMX file
            ToEdmx(
                csdl.ToString(), ssdl.ToString(), msl.ToString(), new FileInfo(
                    modelName + ".edmx"));
        }

        private static void RetrofitModel(string[] args)
        {
            if (args.Length < 4 || args.Length > 5)
            {
                ShowUsage();
                return;
            }

            Generator cedm;
            if (args.Length == 5)
            {
                cedm = new ConceptualEdmGen.Generator(args[1], args[3], args[2], Convert.ToDouble(args[4]));
            }
            else
            {
                cedm = new ConceptualEdmGen.Generator(args[1], args[3], args[2]);
            }
            cedm.Execute();
        }
        
        private static void CodeGen(string[] args)
        {
            if (args.Length != 3)
            {
                ShowUsage();
                return;
            }

            FileInfo edmxFile = null;
            LanguageOption languageOption;

            if (ParseLanguageOption(args[1], out languageOption))
            {
                if (ParseEdmxFileArguments(args[2], out edmxFile))
                {
                    CodeGen(edmxFile, languageOption);
                }
            }
        }

        private static void CodeGen(FileInfo edmxFile, LanguageOption languageOption)
        {
            XElement c = GetCsdlFromEdmx(XDocument.Load(edmxFile.FullName));

            // generate code
            StringWriter sw = new StringWriter();
            EntityClassGenerator codeGen = new EntityClassGenerator(languageOption);
            IList<EdmSchemaError> errors = codeGen.GenerateCode(c.CreateReader(), sw);

            // write out code-file
            string outputFileName = GetFileNameWithNewExtension(edmxFile, 
                GetFileExtensionForLanguageOption(languageOption));
            File.WriteAllText(outputFileName, sw.ToString());

            // output errors
            WriteErrors(errors);
        }

        private static void Validate(string[] args)
        {
            FileInfo edmxFile = null;
            if (ParseEdmxFileArguments(args[1], out edmxFile))
            {
                ValidateAndGenerateViews(edmxFile, LanguageOption.GenerateCSharpCode, false);
            }
        }

        private static void ViewGen(string[] args)
        {
            FileInfo edmxFile = null;
            LanguageOption langOpt;
            if (args.Length != 3)
            {
                ShowUsage();
                return;
            }

            if (ParseLanguageOption(args[1], out langOpt))
            {
                if (ParseEdmxFileArguments(args[2], out edmxFile))
                {
                    ValidateAndGenerateViews(edmxFile, langOpt, true);
                }
            }
        }

        private static void ValidateAndGenerateViews(FileInfo edmxFile, LanguageOption languageOption, bool generateViews)
        {
            XDocument doc = XDocument.Load(edmxFile.FullName);
            XElement c = GetCsdlFromEdmx(doc);
            XElement s = GetSsdlFromEdmx(doc);
            XElement m = GetMslFromEdmx(doc);

            // load the csdl
            XmlReader[] cReaders = { c.CreateReader() };
            IList<EdmSchemaError> cErrors = null;
            EdmItemCollection edmItemCollection = 
                MetadataItemCollectionFactory.CreateEdmItemCollection(cReaders, out cErrors);

            // load the ssdl 
            XmlReader[] sReaders = { s.CreateReader() };
            IList<EdmSchemaError> sErrors = null;
            StoreItemCollection storeItemCollection = 
                MetadataItemCollectionFactory.CreateStoreItemCollection(sReaders, out sErrors);

            // load the msl
            XmlReader[] mReaders = { m.CreateReader() };
            IList<EdmSchemaError> mErrors = null;
            StorageMappingItemCollection mappingItemCollection = 
                MetadataItemCollectionFactory.CreateStorageMappingItemCollection(
                edmItemCollection, storeItemCollection, mReaders, out mErrors);

            // either pre-compile views or validate the mappings
            IList<EdmSchemaError> viewGenerationErrors = null;
            if (generateViews)
            {
                // generate views & write them out to a file
                string outputFile =
                    GetFileNameWithNewExtension(edmxFile, ".GeneratedViews" +
                        GetFileExtensionForLanguageOption(languageOption));
                EntityViewGenerator evg = new EntityViewGenerator(languageOption);
                viewGenerationErrors =
                    evg.GenerateViews(mappingItemCollection, outputFile);
            }
            else
            {
                viewGenerationErrors = EntityViewGenerator.Validate(mappingItemCollection);
            }

            // write errors
            WriteErrors(cErrors);
            WriteErrors(sErrors);
            WriteErrors(mErrors);
            WriteErrors(viewGenerationErrors);

        }

        private static void ToEdmx(string[] args)
        {
            FileInfo cFile, mFile, sFile;
            if (ParseCMSFileArguments(args, out cFile, out sFile, out mFile))
            {
                ToEdmx(cFile, sFile, mFile);
            }
        }

        private static void ToEdmx(FileInfo cFile, FileInfo sFile, FileInfo mFile)
        {
            FileInfo outputFile = new FileInfo(
                GetFileNameWithNewExtension(mFile, ".edmx"));
            ToEdmx(
                File.ReadAllText(cFile.FullName), File.ReadAllText(sFile.FullName), 
                File.ReadAllText(mFile.FullName), outputFile);
        }

        private static void ToEdmx(String c, String s, String m, FileInfo edmxFile)
        {
            // This will strip out any of the xml header info from the xml strings passed in 
            XDocument cDoc = XDocument.Load(new StringReader(c));
            c = cDoc.Root.ToString();
            XDocument sDoc = XDocument.Load(new StringReader(s));
            s = sDoc.Root.ToString();
            XDocument mDoc = XDocument.Load(new StringReader(m));
            // re-write the MSL so it will load in the EDM designer
            FixUpMslForEDMDesigner(mDoc.Root);
            m = mDoc.Root.ToString();

            StringBuilder sb = new StringBuilder();
            sb.Append("<edmx:Edmx Version=\"1.0\"");
            sb.Append(" xmlns:edmx=\"http://schemas.microsoft.com/ado/2007/06/edmx\">");
            sb.Append(Environment.NewLine);
            sb.Append("<edmx:Runtime>");
            sb.Append(Environment.NewLine);
            sb.Append("<edmx:StorageModels>");
            sb.Append(Environment.NewLine);
            sb.Append(s);
            sb.Append(Environment.NewLine);
            sb.Append("</edmx:StorageModels>");
            sb.Append(Environment.NewLine);
            sb.Append("<edmx:ConceptualModels>");
            sb.Append(Environment.NewLine);
            sb.Append(c);
            sb.Append(Environment.NewLine);
            sb.Append("</edmx:ConceptualModels>");
            sb.Append(Environment.NewLine);
            sb.Append("<edmx:Mappings>");
            sb.Append(Environment.NewLine);
            sb.Append(m);
            sb.Append(Environment.NewLine);
            sb.Append("</edmx:Mappings>");
            sb.Append(Environment.NewLine);
            sb.Append("</edmx:Runtime>");
            sb.Append(Environment.NewLine);
            sb.Append("<edmx:Designer xmlns=\"http://schemas.microsoft.com/ado/2007/06/edmx\">");
            sb.Append(Environment.NewLine);
            sb.Append("<Connection><DesignerInfoPropertySet><DesignerProperty Name=\"MetadataArtifactProcessing\" Value=\"EmbedInOutputAssembly\" /></DesignerInfoPropertySet></Connection>");
            sb.Append(Environment.NewLine);
            sb.Append("<edmx:Options />");
            sb.Append(Environment.NewLine);
            sb.Append("<edmx:Diagrams />");
            sb.Append(Environment.NewLine);
            sb.Append("</edmx:Designer>");
            sb.Append("</edmx:Edmx>");
            sb.Append(Environment.NewLine);

            File.WriteAllText(edmxFile.FullName, sb.ToString());
        }
        #endregion

        #region Code to extract the csdl, ssdl & msl sections from an EDMX file

        private static XElement GetCsdlFromEdmx(XDocument xdoc)
        {
            return (from item in xdoc.Descendants(
                        XName.Get("Schema", csdlNamespace)) select item).First();
        }

        private static XElement GetSsdlFromEdmx(XDocument xdoc)
        {
            return (from item in xdoc.Descendants(
                        XName.Get("Schema", ssdlNamespace)) select item).First();
        }

        private static XElement GetMslFromEdmx(XDocument xdoc)
        {
            return (from item in xdoc.Descendants(
                        XName.Get("Mapping", mslNamespace)) select item).First();
        }

        #endregion

        #region Command-line parsing & validation methods

        private static bool ParseEdmxFileArguments(
            string arg, out FileInfo fileInfo)
        {
            string edmxFile = arg;
            fileInfo = new FileInfo(edmxFile);
            if (!fileInfo.Exists)
            {
                System.Console.WriteLine("input file " + edmxFile + " does not exist");
                return false;
            }
            return true;
        }

        private static bool ParseCMSFileArguments(
            string[] args, out FileInfo cFile, out FileInfo sFile, out FileInfo mFile)
        {
            cFile = sFile = mFile = null;
            if (args.Length != 4)
            {
                ShowUsage();
                return false;
            }

            for (int i = 1; i < args.Length; i++)
            {
                if (args[i].EndsWith(".csdl", StringComparison.OrdinalIgnoreCase))
                {
                    cFile = new FileInfo(args[i]);
                }

                if (args[i].EndsWith(".ssdl", StringComparison.OrdinalIgnoreCase))
                {
                    sFile = new FileInfo(args[i]);
                }

                if (args[i].EndsWith(".msl", StringComparison.OrdinalIgnoreCase))
                {
                    mFile = new FileInfo(args[i]);
                }
            }

            if (cFile == null)
            {
                Console.WriteLine("Error:  csdl file not specified");
            }
            if (sFile == null)
            {
                Console.WriteLine("Error:  ssdl file not specified");
            }
            if (mFile == null)
            {
                Console.WriteLine("Error:  msl file not specified");
            }

            if (!cFile.Exists)
            {
                Console.WriteLine("Error:  file " + cFile.FullName + " does not exist");
            }
            if (!sFile.Exists)
            {
                Console.WriteLine("Error:  file " + sFile.FullName + " does not exist");
            }
            if (!mFile.Exists)
            {
                Console.WriteLine("Error:  file " + mFile.FullName + " does not exist");
            }

            if (cFile == null || sFile == null || mFile == null || 
                !cFile.Exists || !sFile.Exists || !mFile.Exists)
            {
                return false;
            }

            return true;
        }

        private static Mode GetMode(string arg)
        {
            if ("/FromEdmx".Equals(arg, StringComparison.OrdinalIgnoreCase))
            {
                return Mode.FromEdmx;
            }
            else if ("/ToEdmx".Equals(arg, StringComparison.OrdinalIgnoreCase))
            {
                return Mode.ToEdmx;
            }
            else if ("/ModelGen".Equals(arg, StringComparison.OrdinalIgnoreCase))
            {
                return Mode.ModelGen;
            }
            else if ("/ViewGen".Equals(arg, StringComparison.OrdinalIgnoreCase))
            {
                return Mode.ViewGen;
            }
            else if ("/CodeGen".Equals(arg, StringComparison.OrdinalIgnoreCase))
            {
                return Mode.CodeGen;
            }
            else if ("/Validate".Equals(arg, StringComparison.OrdinalIgnoreCase))
            {
                return Mode.Validate;
            }
            else if ("/RetrofitModel".Equals(arg, StringComparison.OrdinalIgnoreCase))
            {
                return Mode.RetrofitModel;
            }
            else
            {
                return Mode.Help;
            }
        }

        private static bool ParseLanguageOption(string arg, out LanguageOption langOption)
        {
            langOption = LanguageOption.GenerateCSharpCode;
            if ("vb".Equals(arg, StringComparison.OrdinalIgnoreCase))
            {
                langOption = LanguageOption.GenerateVBCode;
                return true;
            }
            else if ("cs".Equals(arg, StringComparison.OrdinalIgnoreCase))
            {
                langOption = LanguageOption.GenerateCSharpCode;
                return true;
            }
            else
            {
                ShowUsage();
                return false;
            }
        }

        #endregion

        #region Some utility functions we use in the program

        private static string GetFileNameWithNewExtension(
            FileInfo file, string extension)
        {
            string prefix = file.Name.Substring(
                0, file.Name.Length - file.Extension.Length);
            return prefix + extension;
        }

        private static void WriteErrors(IEnumerable<EdmSchemaError> errors)
        {
            if (errors != null)
            {
                foreach (EdmSchemaError e in errors)
                {
                    WriteError(e);
                }
            }
        }

        private static void WriteError(EdmSchemaError e)
        {
            if (e.Severity == EdmSchemaErrorSeverity.Error)
            {
                Console.Write("Error:  ");
            }
            else
            {
                Console.Write("Warning:  ");
            }

            Console.WriteLine(e.Message);
        }

        private static string GetFileExtensionForLanguageOption(
            LanguageOption langOption)
        {
            if (langOption == LanguageOption.GenerateCSharpCode)
            {
                return ".cs";
            }
            else
            {
                return ".vb";
            }
        }

        #endregion

        #region "fix-up" code to fix up MSL so that it will load in the EDMX designer

        //
        // This will re-write MSL to remove some syntax that the EDM Designer 
        // doesn't support.  Specifically, the designer doesn't support 
        //     - the "TypeName" attribute in "EntitySetMapping" elements
        //     - the "StoreEntitySet" attribute in "EntityTypeMapping" and 
        //       "EntitySetMapping" elements.   
        //
        private static void FixUpMslForEDMDesigner(XContainer mappingRoot)
        {
            XName n1 = XName.Get("EntityContainerMapping", mslNamespace);
            XName n2 = XName.Get("EntitySetMapping", mslNamespace);
            XName n3 = XName.Get("EntityTypeMapping", mslNamespace);

            foreach (XElement e1 in SelectDirectDescendents(mappingRoot, n1))
            {
                // process EntitySetMapping nodes
                foreach (XElement e2 in SelectDirectDescendents(e1, n2))
                {
                    XAttribute typeNameAttribute = null;
                    XAttribute storeEntitySetAttribute = null;

                    foreach (XAttribute a in e2.Attributes())
                    {
                        if (a.Name.Equals(XName.Get("TypeName", "")))
                        {
                            typeNameAttribute = a;
                            break;
                        }
                    }

                    if (typeNameAttribute != null)
                    {
                        FixUpEntitySetMapping(typeNameAttribute, e2);
                    }

                    // process EntityTypeMappings
                    foreach (XElement e3 in SelectDirectDescendents(e2, n3))
                    {
                        foreach (XAttribute a in e3.Attributes())
                        {
                            if (a.Name.Equals(XName.Get("StoreEntitySet", "")))
                            {
                                storeEntitySetAttribute = a;
                                break;
                            }
                        }

                        if (storeEntitySetAttribute != null)
                        {
                            FixUpEntityTypeMapping(storeEntitySetAttribute, e3);
                        }
                    }
                }
            }
        }

        private static void FixUpEntitySetMapping(
            XAttribute typeNameAttribute, XElement entitySetMappingNode)
        {
            XName xn = XName.Get("EntityTypeMapping", mslNamespace);

            typeNameAttribute.Remove();
            XElement etm = new XElement(xn);
            etm.Add(typeNameAttribute);

            // move the "storeEntitySet" attribute into the new 
            // EntityTypeMapping node
            foreach (XAttribute a in entitySetMappingNode.Attributes())
            {
                if (a.Name.LocalName == "StoreEntitySet")
                {
                    a.Remove();
                    etm.Add(a);
                    break;
                }
            }

            // now move all descendants into this node
            TransferChildren(entitySetMappingNode, etm);

            entitySetMappingNode.Add(etm);
        }

        private static void FixUpEntityTypeMapping(
            XAttribute storeEntitySetAttribute, XElement entityTypeMappingNode)
        {
            XName xn = XName.Get("MappingFragment", mslNamespace);
            XElement mf = new XElement(xn);

            // move the StoreEntitySet attribute into this node
            storeEntitySetAttribute.Remove();
            mf.Add(storeEntitySetAttribute);

            // now move all descendants into this node
            TransferChildren(entityTypeMappingNode, mf);

            entityTypeMappingNode.Add(mf);
        }

        private static void TransferChildren(
            XContainer originalParent, XContainer newParent)
        {
            // now move all descendants into this node
            List<XNode> childNodes = new List<XNode>();
            foreach (XNode d in originalParent.Nodes())
            {
                childNodes.Add(d);
            }
            foreach (XNode d in childNodes)
            {
                d.Remove();
                newParent.Add(d);
            }
        }

        private static IEnumerable<XElement> SelectDirectDescendents(
            XContainer root, XName name)
        {
            List<XElement> selected = new List<XElement>();
            foreach (XNode node in root.Nodes())
            {
                if (node.NodeType == XmlNodeType.Element)
                {
                    XElement e = node as XElement;
                    if (e != null)
                    {
                        if (e.Name.Equals(name))
                        {
                            selected.Add(e);
                        }
                    }
                }
            }
            return selected;
        }
        #endregion
    }
}
